1. Field of the Invention
The present invention relates generally to a system providing methods for facilitating development and maintenance of software applications or systems, with particular emphasis on a system and methodology for asynchronous code refactoring.
2. Description of the Background Art
Before a digital computer may accomplish a desired task, it must receive an appropriate set of instructions. Executed by the computer's microprocessor, these instructions, collectively referred to as a “computer program,” direct the operation of the computer. Expectedly, the computer must understand the instructions which it receives before it may undertake the specified activity.
Owing to their digital nature, computers essentially only understand “machine code,” i.e., the low-level, minute instructions for performing specific tasks—the sequence of ones and zeros that are interpreted as specific instructions by the computer's microprocessor. Since machine language or machine code is the only language computers actually understand, all other programming languages represent ways of structuring human language so that humans can get computers to perform specific tasks. While it is possible for humans to compose meaningful programs in machine code, practically all software development today employs one or more of the available programming languages. The most widely used programming languages are the “high-level” languages, such C++, Pascal, or more recently Java® and C#. These languages allow data structures and algorithms to be expressed in a style of writing that is easily read and understood by fellow programmers.
A program called a “compiler” translates these instructions into the requisite machine language. In the context of this translation, the program written in the high-level language is called the “source code” or source program.
The ultimate output of the compiler is a compiled module such as a compiled C++“object module,” which includes instructions for execution ultimately by a target processor, or a compiled Java® class, which includes bytecodes for execution ultimately by a Java® virtual machine. A Java® compiler generates platform-neutral “bytecodes”—an architecturally neutral, intermediate format designed for deploying application code efficiently to multiple platforms.
Integrated development environments, such as Borland's JBuilder®, Delphi (trademark) and C# Builder (trademark), are the preferred application development environments for quickly creating production applications. Such environments are characterized by an integrated development environment (IDE) providing a form painter, a property getter/setter manager (“inspector”), a project manager, a tool palette (with objects which the user can drag and drop on forms), an editor, a debugger, and a compiler. In general operation, the user “paints” objects on one or more forms, using the form painter. Attributes and properties of the objects on the forms can be modified using the property manager or inspector. In conjunction with this operation, the user attaches or associates program code with particular objects on the screen (e.g., button object). Typically, code is generated by the IDE in response to user actions in the form painter and the user then manipulates the generated code using the editor. Changes made by the user to code in the editor are reflected in the form painter, and vice versa. After the program code has been developed, the compiler is used to generate binary code (e.g., Java® bytecode) for execution on a machine (e.g., a Java® virtual machine).
Although integrated development environments facilitate the development of applications, issues remain in the development and use of such applications. One problem is that when a large software program or application evolves over time it is common that the initial design gets lost as features that were not in the original specification are added to the application. One way of dealing with this problem of making changes is to design everything with the maximum amount of flexibility. However, this will often lead to unnecessary complexity in the software application, as it is unknown beforehand which parts of the application will require this additional flexibility. Irrespective of how well a system is initially designed or developed, the system is typically modified from time to time during its useful life to improve performance, to accommodate changing needs, to make the system easier to maintain, or for various other reasons.
“Refactoring” is a practice of making structured changes to software applications or systems which add the desired flexibility, but keep the functionality of the system the same. Refactoring involves taking small individual steps that are well defined and that can be applied in succession to yield more significant changes. For example, a developer may wish to perform a “rename refactoring” to change the name of a particular module (e.g., a class name in a Java® program). In order to make this change, the user must locate the definition of this class (i.e., the source code for the class) as well as all uses of the class in other portions of the system. In the case of a class name in a Java® program, the class name is typically used not only for defining a variable, but also for constructing instances (or objects) of that class and accessing static members of the class (i.e., class variables). Another example of refactoring may involve moving a specified class to a new package (referred to as “move refactoring”).
Refactoring of a system may be small or extensive, but even small changes can introduce errors or “defects” into the system. Accordingly, refactoring must be done correctly and completely in order to be effective. Good refactoring requires a mechanism for quickly and accurately identifying definitions and usage of a given symbol in a plurality of source files. The “symbols” that may be involved in refactoring include, for example, package names, class names, interfaces, methods, fields, variables, and properties. Identification of definitions and usage of a given symbol enables refactoring to be performed responsibly and durably so that no defects are introduced and no behavior is changed beyond the desired improvements in features, performance, and/or maintainability.
A particular problem in refactoring a complex application or system is that the application or system may be made up of a number of different modules that are separately developed. Conventionally, creation of a software application or system includes creation of individual source code modules. This approach simplifies program development by dividing functionality available in the program into separate source modules. It should be noted that for purposes of the following discussion the terms “source code” or “source” will refer to source code or any kind of intermediate language representation (IR) of a program. When multiple source modules are employed for creating a program, interdependencies between the individual modules often exist. Program logic in a first module can, for instance, reference variables, methods, objects, and symbols imported from a second module. By the very same token, the first module can also export its own methods, objects, and symbols, making them available for use by other modules. Because of interdependencies between modules, when a particular source module is refactored (e.g., by a developer), the developer must ensure that the modifications made during the refactoring are compatible with the other modules of the program. A particular concern is, therefore, that the changes made during the refactoring might “break” the system, because the change is incompatible with other, dependent modules of the system.
A common problem in the lifecycle of a software system is the ability to change application programming interfaces (APIs) in libraries. Any change of the API in a library which may result from changing source(s) of the library may result in errors in the parsing/attribution of the clients of this library (hereinafter “dependents”). This may limit or prevent the execution of traditional refactorings for the library. For example, a system may include both a library and a separate application module that uses (i.e., is dependent upon) the library. In this case the refactoring of the library may affect the application that is dependent upon the library. Generally, in this environment if refactoring occurs in one module (e.g., the library), then it must also be applied to the other module(s) that are dependent on it (e.g., the application module in this example).
Consider the same example of a library that has one or more applications dependent upon it. The library may, for example, be written and supplied by one company or organization and used by one or more other organizations in various applications. These applications that are written using the library are dependent upon the library. Accordingly, if the library is refactored, the dependent applications will be out of sync with the library and compilation errors may occur when they attempt to use the library. Unless the refactoring relates only to internal aspects of the library that are not used by any dependent applications, when the library is refactored, corresponding changes also need to be made to dependent applications.
The following discussion will use an example of one or more applications that are dependent upon a library. However, it should be noted this is only one example of items of software that are dependent upon one another. Other examples include, but are not limited to: a client portion of an application and a server portion of the application as well as a user interface (UI) portion of a system and a non-UI portion of the system. Various different source components of a software system (e.g., application) may be developed by different developers that may be geographically distributed in different parts of the world. When these components are integrated into a system they need to work with each other and changes made to one component may impact other components of the system.
This problem is typically addressed in one of the following ways in current systems. One approach is to manually refactor the source dependent module(s) to implement the same changes made to the refactored dependee module. For instance, in the case of a refactoring of a library on which an application is dependent, this would generally involve building the application against the refactored library to find compiler errors and then manually identifying and fixing all of these errors. Disadvantages of this approach include that this manual refactoring solution is both tedious and error prone. A developer has to find all the errors (e.g., by building the application against the library and finding compiler errors) and then manually fix them. He or she might miss some of the errors or may otherwise introduce incorrect behavior. In some cases the application may compile, even though underlying errors remain that were not addressed. Another problem is the possibility that new errors may be introduced during the manual fix-up process.
Another possible approach is to modify the library (dependee module) in such a way that the old behavior is maintained. For example, it may possible to do so by providing for duplication in the refactored library. This may involve retaining the existing functionality (before the refactoring) in the library so as to continue to provide support to any dependent modules (e.g., the application in this example). The new or additional functionality provided by the refactoring would then also be provided—for example with different names and with somewhat different behavior.
Another similar alternative is to provide an abstract layer between the application and the (newly refactored) library. The abstract layer can serve to forward the calls to the appropriate routines of the refactored library. However, this approach also adds considerable complexity to the library and may serve to make the refactoring impractical in many cases. Disadvantages of these approaches of supporting multiple interfaces or to retrofitting support for old versions into the newly refactored library include that they will cause the library to become more complex and, therefore, may also make it more fragile and susceptible to error. Use of either of these approaches also makes the library more difficult to design and implement and may take away many of the advantages of refactoring the library in the first place.
This is, in fact, another common alternative. In order to avoid these type of compatibility problems, users often simply elect not to perform any refactoring of the library (or other dependee module) in the first place. This avoids the above-described compatibility problems, but has the disadvantage of limiting the ability to implement needed changes.
What is needed is a solution that facilitates refactoring of a plurality of interdependent modules. The solution should enable refactorings of these modules to be performed even though the modules may be included in separate projects. The solution should also automate the refactoring process so as to improve productivity and reduce the possibility of error. Ideally, the solution should enable a first program or module to be refactored at one time and at a later date facilitate the application of the appropriate changes to a second module that is dependent upon the first one. The present invention provides a solution for these and other needs.